﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Text.RegularExpressions;
using AMO = Microsoft.AnalysisServices;

namespace AmoExtensions
{
    /*
     * SSAS Partition Manager 
     * https://ssaspartitionmanager.codeplex.com/
     * Written by Dr. John Tunnicliffe of Decision Analytics, April 2015
     * eMail: john@decision-analytics.co.uk 
     * http://www.decision-analytics.co.uk/
     * https://www.linkedin.com/in/drjohntunnicliffe
     */

    /// <summary>
    /// The class that actually does the work of creating partitions
    /// </summary>
    public class Partition : IPartition
    {
        #region Properties
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public const string SsasServerNotConnected = "SSAS Server not connected";

        private readonly IAmoServer Server;
        
        private bool _isValidPartitionDefinition = false;

        private const string RegexMatchWhereOneEqualsZero = @"1\s?=\s?0"; 

        private AMO.Database _database = null;

        private AMO.Cube _cube = null; 

        private AMO.MeasureGroup _measureGroup = null;

        public string DatabaseName { get; set; }

        public string ModelType { get; set; }

        public string CubeName { get; set; }

        public string MeasureGroupName { get; set; }

        public string PartitionName { get; set; }

        public string PartitionWhereClause { get; set; }
        
        public string PartitionSlice { get; set; }

        public bool IsValidPartitionDefinition { get { return _isValidPartitionDefinition; } }

        /// <summary>
        /// Returns the ID to be used for the new partition
        /// </summary>
        public string PartitionID { get { return (PartitionName); } }
        #endregion

        #region MeasureGroup Property
        /// <summary>
        /// Returns a reference to the measure group in which the partition is to reside
        /// </summary>
        private AMO.MeasureGroup MeasureGroup
        {
            get
            {
                if (_measureGroup == null)
                {
                    string msg = string.Format("Error occurred while attempting to find measure group '{0}' in cube '{1}' in database '{2}'.", this.MeasureGroupName, this.CubeName, this.DatabaseName);
                    _database = Server.Database(this.DatabaseName);
                    if (_database != null)
                    {
                        _cube = _database.Cube(this.CubeName);
                        if (_cube != null)
                            _measureGroup = _cube.MeasureGroup(this.MeasureGroupName);
                        else
                            throw new ApplicationException(msg);
                    }
                    else
                        throw new ApplicationException(msg);
                }
                return (_measureGroup);
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initialises a partition class, setting the OLAP server
        /// </summary>
        /// <param name="server">A reference to an AMO.Server</param>
        public Partition(IAmoServer server)
        {
            if (server == null)
            {
                throw new ArgumentNullException("server");
            }

            if (!server.Connected)
            {
                throw new ApplicationException(SsasServerNotConnected);
            }

            this.Server = server;
        }
        #endregion

        #region Exists
        /// <summary>
        /// Returns true if the partition exists
        /// </summary>
        public bool Exists()
        {
            // we do this here so we don't accidentally swallow the error where the MeasureGroup does not exist
            AMO.MeasureGroup amoMeasureGroup = MeasureGroup;
            try
            {
                // FindPartition generates an error if the partition does not exist, so we catch the error and 
                AMO.Partition partition = amoMeasureGroup.FindPartition(this.PartitionName);
                // the partition exists
                return true;
            }
            catch
            {
                log.DebugFormat("Partition '{0}' in measure group '{1}' in cube '{2}' in database '{3}'.", this.PartitionName, this.MeasureGroupName, this.CubeName, this.DatabaseName);
                return false;
            }                
        }
        #endregion

        #region IsValid
        /// <summary>
        /// Checks if the partition has a valid definition in a valid measure group. 
        /// 1) Check the database, cube and measure group exist
        /// 2) Check the partition definition has a valid WHERE clause
        /// 3) Check the measure group has a template partition
        /// 4) Check the template partition has a suitable where clause
        /// </summary>
        /// <returns></returns>
        public bool IsValid()
        {
            // check that the database, cube and measure group names exist.  If not, then this will raise an error.
            // The following call does this all in one line :-)
            AMO.MeasureGroup amoMeasureGroup = Server.MeasureGroup(this.DatabaseName, this.CubeName, this.MeasureGroupName);

            // check the where clause is set
            if (PartitionWhereClause == null)
            {
                string msg = string.Format("Partition definition '{0}' does not contain a where clause", this.PartitionName);
                throw new ApplicationException(msg);
            }

            // now ensure the measure group has a template partition 
            // again this will raise an error if a template partition is not found
            AMO.Partition templatePartition = amoMeasureGroup.GetTemplatePartition();

            // now ensure that template has a where clause in the style WHERE 1=0 or some acceptable variation
            string sql = ((AMO.QueryBinding)templatePartition.Source).QueryDefinition;
            if (HasValidWhereClause(sql))
            {
                _isValidPartitionDefinition = true;
            }
            else
            {
                string msg = string.Format("Template partition '{0}' does not contain a where clause in the format WHERE 1=0", templatePartition.Name);
                throw new ApplicationException(msg);
            }
            return _isValidPartitionDefinition;
        }
        #endregion

        #region Create
        /// <summary>
        /// Creates a partition in the measure group if it does not already exist
        /// </summary>
        public bool Create()
        {
            bool returnValue = false;  

            if (!Exists())  // the partition does not exist, so create
            {
                AMO.Partition templatePartition = MeasureGroup.GetTemplatePartition();

                // create a new partition by cloning the template partition
                AMO.Partition partition = templatePartition.Clone();
                partition.ID = this.PartitionID;
                partition.Name = this.PartitionName;
                partition.Description = this.PartitionName;

                AMO.QueryBinding qb = (AMO.QueryBinding)partition.Source;
                
                string sql = ((AMO.QueryBinding)templatePartition.Source).QueryDefinition;
                if (HasValidWhereClause(sql))
                {
                    Regex rgx = new Regex(RegexMatchWhereOneEqualsZero);
                    qb.QueryDefinition = rgx.Replace(sql, this.PartitionWhereClause);
                }
                else
                {
                    // The validation phase should pick up this error, so we should never reach this point
                    string msg = string.Format("Template partition '{0}' does not contain a where clause in the format WHERE 1=0.  Partition not created.", templatePartition.Name);
                    throw new ApplicationException(msg);
                }

                // only do the following steps if we are working with a multidimensional SSAS database
                if (this._database.ModelType == AMO.ModelType.Multidimensional)
                {
                    // set the partition slice 
                    if (!this.PartitionSlice.IsNullOrEmptyOrWhitespace())
                        partition.Slice = this.PartitionSlice;
                    else
                        log.WarnFormat("PartitionSlice not set for partition: {0}", PartitionName);

                    // copy aggregation design from template (if it has one!)
                    if (templatePartition.AggregationDesignID != null)
                    {
                        partition.AggregationDesignID = templatePartition.AggregationDesignID;
                    }
                    else
                    {
                        log.WarnFormat("No aggregations present on template partition: {0}", templatePartition.Name);
                    }
                }

                partition.State = AMO.AnalysisState.Unprocessed;  // we MUST set this as SSAS AMO fails to set it correctly

                MeasureGroup.Partitions.Add(partition);

                returnValue = true;

                log.InfoFormat("Added partition: {0}", PartitionName);


                /*
                 * The Update() method commits the partition to the cube structure.  However, this step takes a very long time,
                 * so we simply call the Update() method on the cube after we have done all our partition manipulations.
                 */
                // partition.Update(); 
            }
            return returnValue;
        }
        #endregion

        #region HasValidWhereClause
        /// <summary>
        /// Helper function for the CreatePartition and IsValid methods
        /// </summary>
        /// <param name="sql">The sql command to test</param>
        /// <returns>True if the sql contains where 1=0</returns>
        public bool HasValidWhereClause(string sql)
        {
            if (sql == null)
            {
                throw new ArgumentNullException("sql");
            }

            // check we have a where clause in the query
            if (sql.ToLower().Contains("where"))
            {
                // check if the where clause has 1=0 or 1 = 0
                return Regex.IsMatch(sql, RegexMatchWhereOneEqualsZero, RegexOptions.IgnoreCase);
            }
            return false;
        }
        #endregion

        #region Process
        /// <summary>
        /// Calls the Process method on the partition object.
        /// Note that this will do nothing if the StartCaptureLog method has been called beforehand until the ExecuteCaptureLog method is called.
        /// </summary>
        /// <param name="partitionName">Name of the partition.</param>
        public void Process()
        {
            // find the partition
            AMO.Partition partition = MeasureGroup.FindPartition(this.PartitionName);
            if (partition != null)
            {
                // call the process method.  However, if we are capturing XMLA, this will not do anything now
                partition.Process(AMO.ProcessType.ProcessFull);
            }
        }
        #endregion

        #region Delete
        /// <summary>
        /// Deletes the partition.
        /// </summary>
        public bool Delete()
        {
            bool returnValue = false;
            // find the partition
            AMO.Partition partitionToBeDeleted = MeasureGroup.FindPartition(this.PartitionName);
            if (partitionToBeDeleted != null)
            {
                MeasureGroup.Partitions.Remove(partitionToBeDeleted);
                returnValue = true;
            }
            return returnValue; 
        }
        #endregion

        #region FromDynamic
        /// <summary>
        /// Creates a new instance of a Partition object from a dynamic type
        /// </summary>
        /// <param name="source">The dynamic variable</param>
        /// <returns>Partition object</returns>
        public static IPartition FromDynamic(IAmoServer server, dynamic source)
        {
            try
            {
                return new Partition(server)
                {
                    DatabaseName = source.SsasDatabaseName,
                    ModelType = source.SsasModelType,
                    CubeName = source.SsasCubeName,
                    MeasureGroupName = source.SsasMeasureGroupName,
                    PartitionName = source.PartitionName,
                    PartitionWhereClause = source.PartitionWhereClause,
                    PartitionSlice = source.PartitionSlice
                };
            }
            catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException rex)
            {
                throw new ApplicationException("Failed to create partition from dynamic object. Reason: " + rex.Message);
            }
        }
        #endregion
    }
}

